#include "transport.h"
#include "logutils.h"

#if defined(PLATFORM_LINUX)
#include "timeout_watch.h"
#endif

#if defined(PLATFORM_WINDOWS)

#define IS_NOT_SUPPORTED_OR_ACCESS_DENIED(errCode) \
    ((errCode) == ERROR_NOT_SUPPORTED || \
     (errCode) == ERROR_ACCESS_DENIED)


static inline bool initOverlapped(OVERLAPPED& ov) {
    if (ov.hEvent == NULL) {
        ov.hEvent = ::CreateEvent(NULL, FALSE, FALSE, NULL);
        if (ov.hEvent == NULL) {
            log_syserror("CreateEvent", ::GetLastError());
            return false;
        }
    }
    return true;
}


SerialTransport::SerialTransport(const char* portName,
    uint32_t baudRate,
    Parity parity,
    uint8_t dataBits,
    StopBits stopBits)
    : m_portName(portName)
    , m_baudRate(baudRate)
    , m_parity(parity)
    , m_dataBits(dataBits)
    , m_stopBits(stopBits)
    , m_fileHandle(INVALID_HANDLE_VALUE)
{
    memset(&m_ovRead, 0, sizeof(m_ovRead));
    memset(&m_ovWrite, 0, sizeof(m_ovWrite));
}


SerialTransport::~SerialTransport()
{
    this->close();
}


TResult SerialTransport::open()
{
    size_t pos = m_portName.rfind('\\');
    if (pos == m_portName.npos)
        m_portName.insert(0, "\\\\.\\");

    HANDLE fileHandle = ::CreateFileA(m_portName.c_str(),
        GENERIC_READ | GENERIC_WRITE, 0, NULL,
        OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL);
    if (fileHandle == INVALID_HANDLE_VALUE) {
        DWORD errCode = ::GetLastError();
        log_syserror("CreateFile", errCode);

        switch (errCode) {
        case ERROR_FILE_NOT_FOUND:
        case ERROR_PATH_NOT_FOUND:
            return TResult::DevNotFound;
        case ERROR_ACCESS_DENIED:
            return TResult::Access;
        default:
            return TResult::OtherError;
        }
    }

    DCB dcb;
    if (!::GetCommState(fileHandle, &dcb)) {
        log_syserror("GetCommState", ::GetLastError());
        ::CloseHandle(fileHandle);
        return TResult::OtherError;
    }

    dcb.BaudRate = m_baudRate;
    dcb.ByteSize = m_dataBits;

    switch (m_parity) {
    case Parity::None:
        dcb.Parity = NOPARITY;
        break;
    case Parity::Odd:
        dcb.Parity = ODDPARITY;
        break;
    case Parity::Even:
        dcb.Parity = EVENPARITY;
        break;
    }

    switch (m_stopBits) {
    case StopBits::One:
        dcb.StopBits = ONESTOPBIT;
        break;
    case StopBits::OnePointFive:
        dcb.StopBits = ONE5STOPBITS;
        break;
    case StopBits::Two:
        dcb.StopBits = TWOSTOPBITS;
        break;
    }

    if (!::SetCommState(fileHandle, &dcb)) {
        log_syserror("SetCommState", ::GetLastError());
        ::CloseHandle(fileHandle);
        return TResult::OtherError;
    }

    if (!::SetupComm(fileHandle, 4096, 4096)) {
        log_syserror("SetupComm", ::GetLastError());
        ::CloseHandle(fileHandle);
        return TResult::OtherError;
    }

    if (!initOverlapped(m_ovRead)) {
        ::CloseHandle(fileHandle);
        return TResult::OtherError;
    }

    if (!initOverlapped(m_ovWrite)) {
        ::CloseHandle(fileHandle);
        return TResult::OtherError;
    }

    m_fileHandle = fileHandle;

    return TResult::NoError;
}


TResult SerialTransport::close()
{
    if (m_fileHandle != INVALID_HANDLE_VALUE) {
        if (!::CloseHandle(m_fileHandle)) {
            log_syserror("CloseHandle", ::GetLastError());
            return TResult::OtherError;
        }
        m_fileHandle = INVALID_HANDLE_VALUE;
    }
    if (m_ovRead.hEvent != NULL) {
        ::CloseHandle(m_ovRead.hEvent);
        memset(&m_ovRead, 0, sizeof(m_ovRead));
    }
    if (m_ovWrite.hEvent != NULL) {
        ::CloseHandle(m_ovWrite.hEvent);
        memset(&m_ovWrite, 0, sizeof(m_ovWrite));
    }

    return TResult::NoError;
}


TResult SerialTransport::read(uint8_t* buf, int bytesToRead, long timeoutVal)
{
    DWORD bytesRead, waitRet, errCode;

    COMMTIMEOUTS timeouts {};
    timeouts.ReadIntervalTimeout = 100;
    timeouts.ReadTotalTimeoutMultiplier = 10;
    timeouts.ReadTotalTimeoutConstant = timeoutVal;

    if (!::SetCommTimeouts(m_fileHandle, &timeouts)) {
        errCode = ::GetLastError();
        log_syserror("SetCommTimeouts", errCode);

        if (IS_NOT_SUPPORTED_OR_ACCESS_DENIED(errCode))
            return TResult::ConnLost;
        return TResult::OtherError;
    }

    if (!::ReadFile(m_fileHandle, buf, bytesToRead, &bytesRead, &m_ovRead)) {
        errCode = ::GetLastError();
        if (IS_NOT_SUPPORTED_OR_ACCESS_DENIED(errCode)) {
            log_syserror("ReadFile", errCode);
            return TResult::ConnLost;
        }

        if (errCode != ERROR_IO_PENDING) {
            log_syserror("ReadFile", errCode);
            return TResult::IoError;
        }

        waitRet = ::WaitForSingleObject(m_ovRead.hEvent, timeoutVal);
        switch (waitRet) {
        case WAIT_OBJECT_0:
            if (!::GetOverlappedResult(m_fileHandle, &m_ovRead, &bytesRead, FALSE)) {
                errCode = ::GetLastError();
                log_syserror("GetOverlappedResult", errCode);

                if (IS_NOT_SUPPORTED_OR_ACCESS_DENIED(errCode))
                    return TResult::ConnLost;
                return TResult::OtherError;
            }
            break;

        case WAIT_TIMEOUT:
            log_error("WaitForSingleObject 超时, timeoutVal = %d", timeoutVal);
            ::CancelIoEx(m_fileHandle, &m_ovRead);
            return TResult::TimedOut;

        default:
            log_error("WaitForSingleObject 失败, waitRet = %d", waitRet);
            ::CancelIoEx(m_fileHandle, &m_ovRead);
            return TResult::OtherError;
        }
    }

    log_rxdata(buf, (int)bytesRead);

    return TResult::NoError;
}


TResult SerialTransport::write(const uint8_t* buf, int bytesToWrite, long timeoutVal)
{
    DWORD bytesWritten, waitRet, errCode;

    COMMTIMEOUTS timeouts {};
    timeouts.WriteTotalTimeoutMultiplier = 10;
    timeouts.WriteTotalTimeoutConstant = timeoutVal;

    if (!::SetCommTimeouts(m_fileHandle, &timeouts)) {
        errCode = ::GetLastError();
        log_syserror("SetCommTimeouts", errCode);

        if (IS_NOT_SUPPORTED_OR_ACCESS_DENIED(errCode))
            return TResult::ConnLost;
        return TResult::OtherError;
    }

    if (!::WriteFile(m_fileHandle, buf, bytesToWrite, &bytesWritten, &m_ovWrite)) {
        errCode = ::GetLastError();
        if (IS_NOT_SUPPORTED_OR_ACCESS_DENIED(errCode)) {
            log_syserror("WriteFile", errCode);
            return TResult::ConnLost;
        }

        if (errCode != ERROR_IO_PENDING) {
            log_syserror("WriteFile", errCode);
            return TResult::IoError;
        }

        waitRet = ::WaitForSingleObject(m_ovWrite.hEvent, timeoutVal);
        switch (waitRet) {
        case WAIT_OBJECT_0:
            if (!::GetOverlappedResult(m_fileHandle, &m_ovWrite, &bytesWritten, FALSE)) {
                errCode = ::GetLastError();
                log_syserror("GetOverlappedResult", errCode);

                if (IS_NOT_SUPPORTED_OR_ACCESS_DENIED(errCode))
                    return TResult::ConnLost;
                return TResult::OtherError;
            }
            break;

        case WAIT_TIMEOUT:
            log_error("WaitForSingleObject 超时, timeoutVal = %d", timeoutVal);
            ::CancelIoEx(m_fileHandle, &m_ovWrite);
            return TResult::TimedOut;

        default:
            log_error("WaitForSingleObject 失败, waitRet = %d", waitRet);
            ::CancelIoEx(m_fileHandle, &m_ovWrite);
            return TResult::OtherError;
        }
    }

    log_txdata(buf, (int)bytesWritten);

    return TResult::NoError;
}


void SerialTransport::discardIoBuffers()
{
    const DWORD flags = PURGE_TXABORT | PURGE_TXCLEAR | PURGE_RXABORT | PURGE_RXCLEAR;
    PurgeComm(m_fileHandle, flags);
}


#else


SerialTransport::SerialTransport(const char* portName,
    uint32_t baudRate,
    Parity parity,
    uint8_t dataBits,
    StopBits stopBits)
    : m_portName(portName)
    , m_baudRate(baudRate)
    , m_parity(parity)
    , m_dataBits(dataBits)
    , m_stopBits(stopBits)
    , m_fileHandle(-1)
{
}


SerialTransport::~SerialTransport()
{
    this->close();
}


TResult SerialTransport::open()
{
    int fd = ::open(m_portName.c_str(), O_RDWR | O_NOCTTY | O_NONBLOCK);
    if (fd == -1) {
        log_syserror("open", errno);
        switch (errno) {
        case ENOENT:
        case ENODEV: return TResult::DevNotFound;;
        case EACCES: return TResult::Access;
        default: return TResult::OtherError;
        }
    }

    struct termios options;
    speed_t speed;

    if (::tcgetattr(fd, &options) != 0) {
        log_syserror("tcgetattr", errno);
        ::close(fd);
        return TResult::OtherError;
    }

    switch (m_baudRate) {
    case 1200: speed = B1200; break;
    case 2400: speed = B2400; break;
    case 4800: speed = B4800; break;
    case 9600: speed = B9600; break;
    case 19200: speed = B19200; break;
    case 38400: speed = B38400; break;
    case 57600: speed = B57600; break;
    case 115200: speed = B115200; break;
    default: speed = B9600; break;
    }

    log_error("speed : %d", m_baudRate);

    if (::cfsetispeed(&options, speed) != 0) {
        log_syserror("cfsetispeed", errno);
        ::close(fd);
        return TResult::OtherError;
    }

    if (::cfsetospeed(&options, speed) != 0) {
        log_syserror("cfsetospeed", errno);
        ::close(fd);
        return TResult::OtherError;
    }

    ::cfmakeraw(&options);

    if (m_parity != Parity::None) {
        options.c_cflag |= PARENB;  /* enable parity bit */

        if (m_parity == Parity::Odd)
            options.c_cflag |= PARODD;
        else if (m_parity == Parity::Even)
            options.c_cflag &= ~PARODD;
    }

    if (m_dataBits < 8) {
        options.c_cflag &= ~CSIZE;

        if (m_dataBits == 5)
            options.c_cflag |= CS5;
        else if (m_dataBits == 6)
            options.c_cflag |= CS6;
        else if (m_dataBits == 7)
            options.c_cflag |= CS7;
    }

    options.c_cflag &= ~CSTOPB;
    if (m_stopBits != StopBits::One) {
        if (m_stopBits == StopBits::Two)
            options.c_cflag |= CSTOPB;
    }

    options.c_cc[VTIME] = 1;
    options.c_cc[VMIN] = 1;

    if (::tcsetattr(fd, TCSANOW, &options) != 0) {
        log_syserror("tcsetattr", errno);
        ::close(fd);
        return TResult::OtherError;
    }

    m_fileHandle = fd;

    return TResult::NoError;
}


TResult SerialTransport::close()
{
    if (m_fileHandle != -1) {
        int ret = ::close(m_fileHandle);
        if (ret != 0) {
            log_syserror("close", errno);
            return TResult::OtherError;
        }
    }

    return TResult::NoError;
}


static inline int selectEx(int fd, fd_set* readfds, fd_set* writefds, long timeoutVal)
{
    timeval tv;
    tv.tv_sec = timeoutVal / 1000;
    tv.tv_usec = timeoutVal % 1000 * 1000;
    log_info("tv is %d, %d", tv.tv_sec, tv.tv_usec);
    return ::select(fd, readfds, writefds, nullptr, &tv);
}


TResult SerialTransport::read(uint8_t* buf, int bytesToRead, long timeoutVal)
{
    fd_set fds;
    int ret, bytesRead;
    TimeoutWatch tw(timeoutVal);

    do {
        FD_ZERO(&fds);
        FD_SET(m_fileHandle, &fds);

        ret = selectEx(m_fileHandle + 1, &fds, nullptr, tw.remainingTime());
        FD_CLR(m_fileHandle, &fds);

        if (ret == -1) {
            log_syserror("select", errno);
            return TResult::OtherError;
        }

        if (ret == 0) {
            log_error("select 超时");
            return TResult::TimedOut;
        }

        bytesRead = ::read(m_fileHandle, buf, bytesToRead);
        if (bytesRead > 0) {
            log_rxdata(buf, bytesRead);

            buf += bytesRead;
            bytesToRead -= bytesRead;
        }
        else {
            if (bytesRead == -1) {
                log_syserror("read", errno);

                if (errno == EINTR)
                    return TResult::ConnLost;
                if (errno != EAGAIN)
                    return TResult::IoError;
            }

            ::usleep(1000); // 1ms
        }
    } while (bytesToRead);

    return TResult::NoError;
}


TResult SerialTransport::write(const uint8_t* buf, int bytesToWrite, long timeoutVal)
{
    fd_set fds;
    int ret, bytesWritten;
    TimeoutWatch tw(timeoutVal);

    do {
        FD_ZERO(&fds);
        FD_SET(m_fileHandle, &fds);
        log_info("m_fileHandle is %d,tw.remainingTime() is %d", m_fileHandle, tw.remainingTime());
        ret = selectEx(m_fileHandle + 1, nullptr, &fds, tw.remainingTime());
        log_info("selectEx ret is %d", ret);
        FD_CLR(m_fileHandle, &fds);
        log_info("484", ret);
        if (ret == -1) {
            log_syserror("select", errno);
            return TResult::OtherError;
        }

        if (ret == 0) {
            log_error("select 超时");
            return TResult::TimedOut;
        }
        log_info("494", ret);
        bytesWritten = ::write(m_fileHandle, buf, bytesToWrite);
        if (bytesWritten > 0) {
            log_txdata(buf, bytesWritten);

            buf += bytesWritten;
            bytesToWrite -= bytesWritten;
        }
        else {
            if (bytesWritten == -1) {
                log_syserror("write", errno);

                if (errno == EINTR)
                    return TResult::ConnLost;
                if (errno != EAGAIN)
                    return TResult::IoError;
            }

            ::usleep(1000); // 1ms
        }
    } while (bytesToWrite);
    log_info("515", ret);
    return TResult::NoError;
}


void SerialTransport::discardIoBuffers()
{
    ::tcflush(m_fileHandle, TCIOFLUSH);
}

#endif
